Xamarin.Forms では、左からスライドして出てくるメニューを持つ画面を MasterDetailPage
で作成します。
一方、普通に画面遷移していく場合は ContentPage などを NavigationPage
でラップしてあげます。
何がしたいかというと、両者を組み合わせたいんです。こういうことってよくありませんかね?
起動画面で「新規ユーザー登録」があって、「ユーザー登録画面」を経て、メインの画面に遷移する、メイン画面にはスライドメニューがある、というパターン。これを Xamarin.Forms でやりたいのです。
ところが、 NavigationPage
で遷移していく画面の中に MasterDetailPage
があると、 NavigationPage
の方が勝ってしまい、ナビゲーションバーには「BACK」ボタンが表示されてしまいます。
これを消そうと、MasterDetailPage
のコンストラクタで NavigationPage.SetHasBackButton(this, false)
してみます。
その結果がこれ。
Android の方は望む結果になったけど、iOSの方はうーん…、BACKボタンは消えたけど、メニューを表示させるボタンが出ません。
しょうがないので、iOS の場合だけ、ナビゲーションバーの左ボタンをどうにかして追加してみます。
Xamarin.Forms のお供、CustomRenderer です。
MasterDetailPage
の iOS向けCustomRenderer を作って、ネイティブ側でナビゲーションバーをカスタマイズしてみます。
// CustomMasterDetailRenderer.cs
using System;
using MasterDetail.iOS;
using MonoTouch.UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(MasterDetailPage), typeof(CustomMasterDetailRenderer))]
namespace MasterDetail.iOS
{
public class CustomMasterDetailRenderer : PhoneMasterDetailRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var page = Element as MasterDetailPage;
var navigationItem = this.NavigationController.TopViewController.NavigationItem;
navigationItem.LeftBarButtonItems = new UIBarButtonItem[]
{
new UIBarButtonItem("MENU", UIBarButtonItemStyle.Plain, (_, __) =>
{
page.IsPresented = !page.IsPresented;
})
};
}
}
}
「MENU」ってボタンを、ナビゲーションバーの左側に追加しています。 こんな CustomRenderer を iOS 側のプロジェクトに追加して実行してみます。
オーケーオーケー、これが私が求めていたソリューションです。
が、CustomRenderer にはいくつか考えなければならないことがあります。
CustomMasterDetailRenderer.cs
は PhoneMasterDetailRenderer
というクラスを継承しています。が、実はこれは iPhone 用で、実はタブレット(iPad)用に TabletMasterDetailRenderer
というクラスもあります。これの CustomRenderer も用意しなければなりませんか?CustomMasterDetailRenderer
から派生させるしかなくなります。で、 Xamarin.Forms には、v2.1 から既存機能の拡張に Effects という選択肢が加わりました。
では、CustomMasterDetailRenderer.cs
を Effects に変えてみましょう。
// CustomMasterDetailEffect.cs
using System;
using MasterDetail.iOS;
using MonoTouch.UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ResolutionGroupName("mycompany")]
[assembly: ExportEffect(typeof(CustomMasterDetailEffect), "CustomMasterDetailEffect")]
namespace MasterDetail.iOS
{
public class CustomMasterDetailEffect : PlatformEffect
{
protected override void OnAttached()
{
var page = Element as MasterDetailPage;
page.Appearing += Page_Appearing;
}
protected override void OnDetached()
{
var page = Element as MasterDetailPage;
page.Appearing -= Page_Appearing;
}
void Page_Appearing(object sender, EventArgs e)
{
var vc = GetParentViewController();
var page = Element as MasterDetailPage;
var navigationItem = vc.NavigationController.TopViewController.NavigationItem;
navigationItem.LeftBarButtonItems = new UIBarButtonItem[]
{
new UIBarButtonItem("MENU", UIBarButtonItemStyle.Plain, (_, __) =>
{
page.IsPresented = !page.IsPresented;
})
};
}
UIViewController GetParentViewController()
{
UIResponder responder = this.Container;
while ((responder = responder.NextResponder) != null)
{
if (responder is UIViewController)
{
return (UIViewController)responder;
}
}
return null;
}
}
}
Effect は、 ResolutionGroupName
と ExportEffect
で定義した名称を使って、PCL側プロジェクトで Page に追加します。
// RootPage.cs
public class RootPage : MasterDetailPage
{
public RootPage ()
{
NavigationPage.SetHasBackButton(this, false);
// Effect を追加する
Effects.Add(Effect.Resolve("mycompany.CustomMasterDetailEffect"));
// 以下省略
こちらも、CustomRenderer と同じことができました。
が、ちょっと黒魔術っぽいの使ってます。
Effects.Container
から取得できるのは UIView
です。親の UIViewController
を得るには、 GetParentViewController()
でやってるような事をしなければなりませんViewController
だったので ViewWillAppear()
など画面のライフサイクルコールバックを override することができました。が、Effects から ViewController のライフサイクルイベントをハンドリングできません。代わりに Xamarin.Forms 側の Page
のライフサイクルから Appearing
イベントで処理するようにしています。そのため、「MENU」ボタンが表示されるタイミングが若干遅れます。以上を考えると、
となるでしょう。
本件のネタは、 Effects ではかなりムリをして実現しているので、CustomRenderer の方が相応しいと思われます。 が、CustomRenderer はここぞという時にとっておきたい気もします。 このさじ加減は、作るアプリの規模・深度、汎用性、再利用性などによって変わってくるでしょう。Effects の方が汎用性・再利用性は高いですが、ネイティブのUIパーツをごっそり入れ替えるような深い事は、CustomRenderer でなければできません。
今回のプログラムは Github に上げてあります。(CustomMasterDetailRenderer.cs
はコメントアウトしてあって、Effects の方を活かしてます。)
私は「Effects で頑張りたい派」かな。